Implied volatility (IV) represents the market’s expectation of future price fluctuations, derived from the prices of options.
It is a forward-looking metric, unlike realized volatility, which measures past price movements.
Around corporate earnings announcements, IV tends to rise as uncertainty about future financial results increases.
After the event, this uncertainty is resolved, often leading to a “volatility crush” — a sharp decline in IV.
Understanding this behavior is crucial for traders and researchers, as it provides insights into:
In this notebook, we will:
Goal: Explain why implied volatility (IV) spikes around earnings and how that can be quantified and visualized.
Outline:
What is Implied Volatility (IV)? → Market’s expectation of future volatility, derived from option prices.
Why it spikes before earnings? → Uncertainty about future earnings announcements leads traders to price in larger expected moves.
How it decays post-earnings? → “Volatility crush” as uncertainty resolves.
Our objective: build a small research-based model that identifies these conditions before they occur.
| Study | Key Insight |
|---|---|
| Doran & Krieger (2010) — Journal of Derivatives | IV increases sharply before earnings announcements, especially in short-dated options, and collapses afterward. |
| Pan & Poteshman (2006) — Review of Financial Studies | Option volumes and IV changes contain information about future stock price movements. |
| Xing, Zhang & Zhao (2010) — JFQA | The shape of the volatility smirk before earnings predicts cross-sectional returns. |
| Dennis, Mayhew & Stivers (2006) | Show that pre-event volatility is not fully explained by historical volatility alone — indicating strong risk-premium effects. |
This section visualizes the behavior of implied volatility (IV) and underlying stock prices during earnings events, highlighting the characteristic pre-earnings IV spike and post-earnings IV crush. These patterns provide crucial signals for options traders, guiding both long and short volatility strategies such as straddles and calendar spreads.
import pandas as pd
import yfinance as yf
import plotly.graph_objects as go
# --------------------------------------------------
# User-defined config
# --------------------------------------------------
csv_files = {
'AAPL': 'AAPL.csv',
'MSFT': 'MSFT.csv',
'AMZN': 'AMZN.csv',
'GOOGL': 'GOOG.csv',
'TSLA': 'TSLA.csv'
}
earnings_dates = {
'AAPL': ['2024-10-31'],
'MSFT': ['2024-10-30'],
'AMZN': ['2024-10-31'],
'GOOGL': ['2024-10-29'],
'TSLA': ['2024-10-23']
}
# --------------------------------------------------
# Prepare merged data
# --------------------------------------------------
merged_data = {}
for ticker, path in csv_files.items():
# Load option implied volatility data
df_iv = pd.read_csv(path)
df_iv['date'] = pd.to_datetime(df_iv['date'])
# Calculate average implied vol per day (since multiple strikes)
daily_iv = df_iv.groupby('date', as_index=False)['impliedVolatility'].mean()
# Get stock price data from yfinance (explicitly set auto_adjust)
start, end = df_iv['date'].min(), df_iv['date'].max()
stock_data = yf.download(ticker, start=start, end=end, auto_adjust=False)
# Flatten column index if MultiIndex
if isinstance(stock_data.columns, pd.MultiIndex):
stock_data.columns = [col[0] if isinstance(col, tuple) else col for col in stock_data.columns]
stock_data.reset_index(inplace=True)
stock_data = stock_data[['Date', 'Close']].rename(columns={'Date': 'date'})
# Merge cleanly
merged = pd.merge(daily_iv, stock_data, on='date', how='inner')
merged_data[ticker] = merged
print("Data merged successfully.")
[*********************100%***********************] 1 of 1 completed [*********************100%***********************] 1 of 1 completed [*********************100%***********************] 1 of 1 completed [*********************100%***********************] 1 of 1 completed [*********************100%***********************] 1 of 1 completed
Data merged successfully.
fig = go.Figure()
trace_offsets = {}
trace_count = 0
# Build traces per ticker and keep track of trace indices
for ticker, df in merged_data.items():
idxs = []
# IV trace
fig.add_trace(go.Scatter(
x=df['date'], y=df['impliedVolatility'],
mode='lines', name=f'{ticker} IV',
line=dict(color='blue'), yaxis='y1', visible=False
))
idxs.append(trace_count)
trace_count += 1
# Price trace
fig.add_trace(go.Scatter(
x=df['date'], y=df['Close'],
mode='lines', name=f'{ticker} Price',
line=dict(color='orange'), yaxis='y2', visible=False
))
idxs.append(trace_count)
trace_count += 1
# Earnings vertical lines
for edate in earnings_dates.get(ticker, []):
e_date = pd.to_datetime(edate)
ymin = df['impliedVolatility'].min()
ymax = df['impliedVolatility'].max()
fig.add_trace(go.Scatter(
x=[e_date, e_date],
y=[ymin, ymax],
mode='lines',
line=dict(color='red', dash='dot'),
name=f'{ticker} Earnings',
yaxis='y1',
showlegend=False,
visible=False
))
idxs.append(trace_count)
trace_count += 1
trace_offsets[ticker] = idxs
# Set only the first ticker visible initially
init_ticker = list(trace_offsets.keys())[0]
for idx in trace_offsets[init_ticker]:
fig.data[idx].visible = True
# Construct dropdown menu
buttons = []
for ticker, idxs in trace_offsets.items():
vis = [False] * trace_count
for i in idxs:
vis[i] = True
buttons.append(
dict(
label=ticker,
method='update',
args=[{'visible': vis}, {'title': f"{ticker} - Implied Volatility & Stock Price"}]
)
)
fig.update_layout(
title=f"{init_ticker} - Implied Volatility & Stock Price",
xaxis_title="Date",
yaxis=dict(title="Implied Volatility", side='left'),
yaxis2=dict(title="Stock Price", overlaying='y', side='right'),
updatemenus=[dict(active=0, buttons=buttons, x=0.15, xanchor='left', y=1.15, yanchor='top')],
legend=dict(x=0.02, y=0.98)
)
fig.show()
def iv_crush_summary(merged_data, earnings_dates):
results = []
for ticker, df in merged_data.items():
event_date = pd.to_datetime(earnings_dates[ticker][0])
# IV on last trading day before earnings
pre_mask = df['date'] < event_date
iv_pre = df.loc[pre_mask, 'impliedVolatility'].iloc[-1] if pre_mask.any() else None
# IV on first trading day after earnings
post_mask = df['date'] > event_date
iv_post = df.loc[post_mask, 'impliedVolatility'].iloc[0] if post_mask.any() else None
# Calculate % crush
if iv_pre and iv_post:
crush_pct = (iv_pre - iv_post) / iv_pre * 100
else:
crush_pct = None
results.append({
'Ticker': ticker,
'Event_Date': event_date,
'Pre_Earnings_IV': iv_pre,
'Post_Earnings_IV': iv_post,
'IV_Crush_%': crush_pct
})
return pd.DataFrame(results)
iv_crush_df = iv_crush_summary(merged_data, earnings_dates)
display(iv_crush_df)
| Ticker | Event_Date | Pre_Earnings_IV | Post_Earnings_IV | IV_Crush_% | |
|---|---|---|---|---|---|
| 0 | AAPL | 2024-10-31 | 0.346233 | 0.309886 | 10.497676 |
| 1 | MSFT | 2024-10-30 | 0.353180 | 0.322196 | 8.772917 |
| 2 | AMZN | 2024-10-31 | 0.457490 | 0.331951 | 27.440733 |
| 3 | GOOGL | 2024-10-29 | 0.512718 | 0.330840 | 35.473381 |
| 4 | TSLA | 2024-10-23 | 0.597078 | 0.594118 | 0.495740 |
def pre_earnings_iv_runup_trading(merged_data, earnings_dates, window=5):
results = []
for ticker, df in merged_data.items():
event_date = pd.to_datetime(earnings_dates[ticker][0])
event_idx = df.index[df['date'] == event_date].tolist()
if not event_idx:
continue
last_n_idx = max(event_idx[0] - window, 0)
runup_window = df.iloc[last_n_idx:event_idx[0]]
if not runup_window.empty:
iv_start = runup_window['impliedVolatility'].iloc[0]
iv_end = runup_window['impliedVolatility'].iloc[-1]
pct_change = (iv_end - iv_start) / iv_start * 100 if iv_start > 0 else 0
results.append({'Ticker': ticker, 'Start_IV': iv_start, 'End_IV': iv_end,
'IV_Runup_%': pct_change, 'Event_Date': event_date})
return pd.DataFrame(results)
display(pre_earnings_iv_runup_trading(merged_data, earnings_dates, window=5))
| Ticker | Start_IV | End_IV | IV_Runup_% | Event_Date | |
|---|---|---|---|---|---|
| 0 | AAPL | 0.329804 | 0.346233 | 4.981394 | 2024-10-31 |
| 1 | MSFT | 0.351388 | 0.353180 | 0.510007 | 2024-10-30 |
| 2 | AMZN | 0.450207 | 0.457490 | 1.617668 | 2024-10-31 |
| 3 | GOOGL | 0.415066 | 0.512718 | 23.526852 | 2024-10-29 |
| 4 | TSLA | 0.580355 | 0.597078 | 2.881506 | 2024-10-23 |
Long Straddle (Buy Call + Put at Same Strike)
Objective: Profit from a large move in the underlying stock or a spike in IV before earnings.
How it Works: Buy at-the-money call and put options before earnings to capture the expected IV increase and potential price jump.
Risks: High premium cost; if the stock remains quiet or IV drops suddenly, losses can occur.
Ideal Scenario: Clear IV run-up visible in your analysis signals a favorable entry point.
Long Calendar Spread (Buy Longer-Term, Sell Shorter-Term Option)
Objective: Benefit from IV increasing before earnings in the near-term options while hedging with longer-term options.
How it Works: Buy longer-dated options and sell shorter-dated options at the same strike; gains from heightened near-term IV and potential volatility decay afterward.
Benefits: More cost efficient than long straddles; can profit from IV term structure shapes.
Ideal Scenario: Rising front-month IV compared to back-month IV, as seen in IV term slope analysis.
Short Straddle/Strangle
Objective: Collect premium income from elevated IV before earnings, expecting a consolidating stock price and IV crush after event.
How it Works: Sell at-the-money call and put options; profits from IV crush and time decay post earnings.
Risks: Large risk if the stock moves sharply; requires precise risk management.
Ideal Scenario: Elevated IV with expectation of low realized volatility or range-bound event.
IV Spike Detection: Use visualization and run-up metrics to time entries for long volatility plays.
IV Crush Quantification: Helps evaluate potential premium loss for short volatility plays.
Volatility Term Structure: Analyzing slopes aids strategy selection between calendar spreads vs. straight straddles.
Realized vs Implied Volatility: Understanding this difference improves risk assessment for all strategies.
The Yang-Zhang volatility estimator provides a sophisticated realized volatility measure that accounts for overnight price jumps and drift, improving upon simpler close-to-close methods. In this study, it offers a rigorous benchmark for comparing market-implied expectations versus actual price fluctuations.
The following section introduces and explains the realized volatility formula used to compare against implied volatility (IV).
$$ \sigma_{YZ} \;=\; \sqrt{\; \frac{1}{n - 1}\; \left( \sum_{i=1}^{n} r_{\text{open},i}^2 \;+\; k \sum_{i=1}^{n} r_{\text{close},i}^2 \;+\; (1-k)\sum_{i=1}^{n} RS_i \right) \times 252 \;} $$where:
$$ k \;=\; \frac{0.34}{\,1.34 + \frac{n+1}{n-1}\,} $$and
$$ RS_i \;=\; \ln\!\left(\frac{H_i}{O_i}\right)\! \left[\ln\!\left(\frac{H_i}{O_i}\right) - \ln\!\left(\frac{C_i}{O_i}\right)\right] \;+\; \ln\!\left(\frac{L_i}{O_i}\right)\! \left[\ln\!\left(\frac{L_i}{O_i}\right) - \ln\!\left(\frac{C_i}{O_i}\right)\right] $$where:
For each expiration date (t), we estimate the ATM implied volatility and interpolate it using a spline:
$$ IV_{\text{term}}(dte) = f(dte) $$We compute the slope between the nearest expiry and 45 days:
$$ \text{Slope}_{0-45} \;=\; \frac{IV_{\text{term}}(45) - IV_{\text{term}}(dte_{\min})}{45 - dte_{\min}} $$A negative slope implies front-month options are more expensive — a pattern often observed prior to earnings.
Define the implied-to-realized ratio:
$$ \text{Ratio} \;=\; \frac{IV_{45}}{RV_{45}} $$If : $$ \text{Ratio} > 1.25\ $$ Then the market is implying volatility significantly higher than recent realized volatility — indicating elevated uncertainty.
Our initial volatility modeling utilizes the Yang-Zhang volatility estimator to provide a robust measurement of realized volatility, accounting for overnight price gaps and intraday noise. This methodology lays the foundation for understanding how the market’s realized volatility compares with the forward-looking implied volatility extracted from option prices.
Through our visualizations and quantitative analysis, we observe distinct patterns of IV spikes leading up to earnings and IV crushes immediately following those events. These implied volatility dynamics reflect the market's anticipation and resolution of earnings uncertainty.
Building on this theoretical base, the provided Python code operationalizes trading decision-making by combining key signals:
A rate of increase in implied volatility relative to the realized volatility benchmark (IV45/RV45 ratio),
Term structure slope analysis (IV across different expiration dates)
Underlying stock liquidity (average volume).
Together, these indicators drive a refined recommendation system that suggests when to enter long volatility plays such as straddles or calendar spreads in anticipation of earnings-driven IV spikes.